Carga de librerias

suppressPackageStartupMessages(library(readr))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(lubridate))

Carga de datos

Cargaremos un dataset con los datos referentes a las llamadas del 911, las cuales se encuentran divididas por semestre, notamos que cuenta con algunos datos actualizados de 2020 sin embargo no son muy relevantes.

# Cargar datasets con columna año
df_1_2022 <- read_csv("llamadas_911_2022_s1.csv", show_col_types = FALSE) %>% mutate(anio = 2022)
df_2_2021 <- read_csv("llamadas_911_2021_s2.csv", show_col_types = FALSE) %>% mutate(anio = 2021)
df_1_2021 <- read_csv("llamadas_911_2021_s1.csv", show_col_types = FALSE) %>% mutate(anio = 2021)

Procederemos a unir los 3 conjuntos en un solo data set, para esto deberemos contar con la misma estructura en todos los archivos y para ello usaremos glimpse para dicha revision.

# Unir los datasets
df <- bind_rows(df_1_2022, df_2_2021, df_1_2021)
# Revisar estructura
glimpse(df)
## Rows: 1,671,036
## Columns: 19
## $ folio                  <chr> "C5/20220110/02402", "C5/20220129/01383", "C5/2…
## $ categoria_incidente_c4 <chr> "Medicos", "Derrame o fuga", "Cadaver", "Cadave…
## $ incidente_c4           <chr> "Dolor", "Olor a gas combustible quimicos", "Mu…
## $ anio_creacion          <dbl> 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022,…
## $ mes_creacion           <chr> "Enero", "Enero", "Enero", "Enero", "Enero", "E…
## $ fecha_creacion         <date> 2022-01-10, 2022-01-29, 2022-01-29, 2022-01-29…
## $ hora_creacion          <time> 17:01:37, 10:39:06, 16:28:36, 15:28:21, 14:33:…
## $ anio_cierre            <dbl> 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022,…
## $ mes_cierre             <chr> "Enero", "Enero", "Enero", "Enero", "Enero", "E…
## $ fecha_cierre           <date> 2022-01-10, 2022-01-29, 2022-01-29, 2022-01-29…
## $ hora_cierre            <time> 20:04:48, 13:46:10, 21:16:09, 19:26:09, 20:36:…
## $ codigo_cierre          <chr> "Otros", "Otros", "A", "A", "A", "A", "A", "A",…
## $ clas_con_f_alarma      <chr> "URGENCIAS MEDICAS", "EMERGENCIA", "EMERGENCIA"…
## $ alcaldia_cierre        <chr> "IZTAPALAPA", "IZTACALCO", "IZTAPALAPA", "IZTAP…
## $ colonia_cierre         <chr> "LOS ANGELES", "REFORMA IZTACCIHUATL NORTE", "E…
## $ manzana                <chr> "9.01E+14", "9.01E+14", "9.01E+14", "9.01E+14",…
## $ latitud                <dbl> 19.34255, 19.38376, 19.35514, 19.37824, 19.4334…
## $ longitud               <dbl> -99.06475, -99.12516, -99.09455, -99.09951, -99…
## $ anio                   <dbl> 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022,…

Datos cargados

Revisaremos los 3 data sets originales y el dataset unificado

df_1_2022
df_2_2021
df_1_2021
df

1. Incidentes por mes y alcaldía.

Para esto realizaremos una agrupación por el mes_de creación y los agruparemos con creando un array con el total de meses con estos datos filtraremos en el dataset, procederemos a graficar dicho dataset

# Agrupar por mes y alcaldía
df %>%
  count(mes_creacion, alcaldia_cierre) %>%
  mutate(
    mes_creacion = factor(mes_creacion,
    levels = c("Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
               "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"),
    ordered = TRUE)
  ) %>%
  ggplot(aes(x = mes_creacion, y = n, fill = alcaldia_cierre)) +
  geom_col(position = "dodge") +
  labs(
    title = "Número de incidentes por mes y alcaldía",
    x = "Mes",
    y = "Número de incidentes",
    fill = ""
  ) +
  theme_minimal(base_size = 14) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Observacion: esto nos dejara notar cuales son las alcaldías con mayor reporte de incidentes, Así como dar un seguimiento mensual.

1.1 Número promedio de incidentes por categoría.

Posterior a esto procederemos a realizar un conteo por categorías para identificar cuáles son las que cuentan con mayor índice de reportes

1. Generaremos una tabla con el total de incidentes por categoría.

2. Procederemos a graficar , tal cual el orden de la tabla previamente generada.

# Contar categorías únicas y ordenarlas por frecuencia
categorias <- df %>%
  filter(!is.na(categoria_incidente_c4)) %>%
  count(categoria_incidente_c4, sort = TRUE)

En este análisis podremos observar que los principales incidentes suelen ser disturbios, servicios y agresión. Mientras que el menor suele ser persona extraviada en zonas boscosas.

# Ver resultado
print(categorias, n = Inf)
## # A tibble: 23 × 2
##    categoria_incidente_c4                    n
##    <chr>                                 <int>
##  1 Disturbio                            291628
##  2 Servicios                            271150
##  3 Agresion                             172348
##  4 Administrativas                      161069
##  5 Denuncia                             133330
##  6 Medicos                              126229
##  7 Lesionado                             72815
##  8 Derrame o fuga                        59808
##  9 Incendio                              53509
## 10 Danos por fenomeno natural o tercero  23092
## 11 Danos                                 15275
## 12 Abandono                              14983
## 13 Robo                                  11316
## 14 Cadaver                                9439
## 15 Contra la salud                        6383
## 16 Amenaza                                6353
## 17 Explosion                              3461
## 18 Detencion ciudadana                    3422
## 19 Privacion de la libertad               3011
## 20 Intento de suicidio                    2658
## 21 Sismo                                  2384
## 22 Accidente                                81
## 23 Persona extraviada en zona boscosa       80

Observación

En la grafica previa Numero de incidentes por mes y alcaldia vimos el total de todos los años, así que procederemos a separar por año para ver los incidentes de una forma aislada.

De acuerdo a la relación de tiempo, muchos incidentes han disminuido su frecuencia, sin embargo, puede deberse a muchos factores, entre ellos que elconjunto de datos, no este del todo bien, por los conjuntos de datos y algunas ausencias que se notaronm, Sin embargo, los incidentes de servicios y disturbios siguen siendo relevantes para la mayor cantidad de incidentes.

Observación Graficaremos el total de incidentes para verificar el comportamiento y notaremos claramente cuales son las mas comunes entre ellas disturbios y servicios, sin embargo esto corresponde al total de registros

# Contar y ordenar categorías
categorias <- df %>%
  filter(!is.na(categoria_incidente_c4)) %>%
  count(categoria_incidente_c4, sort = TRUE)

# Gráfico de barras con etiquetas
ggplot(categorias, aes(x = reorder(categoria_incidente_c4, n), y = n)) +
  geom_col(fill = "#0073C2FF") +
  geom_text(aes(label = n), hjust = -0.1, size = 3.5) +  # Agrega los totales
  coord_flip() +
  labs(
    title = "Frecuencia de categorías de incidentes 911",
    x = "Categoría",
    y = "Número de incidentes"
  ) +
  theme_minimal(base_size = 14) +
  theme(plot.title = element_text(hjust = 0.5)) +
  expand_limits(y = max(categorias$n) * 1.05)  # Espacio para que no se corte el número

Para obtener un panorama mas claro, procederemos a generar un grafico, segmentando por año el total de incidentes, notando asi un patron mas claro en la disminucion de incidentes para el año 2022, esto puede deberse a que solo se esta graficando 1 semestre de ese año de acuerdo al conjunto de datos

# Contar categorías por año
categorias_anio <- df %>%
  filter(!is.na(categoria_incidente_c4)) %>%
  count(anio, categoria_incidente_c4)
# Para controlar el orden de las categorías (por total combinado)
orden_categorias <- categorias_anio %>%
  group_by(categoria_incidente_c4) %>%
  summarise(total = sum(n)) %>%
  arrange(total) %>%
  pull(categoria_incidente_c4)
categorias_anio$categoria_incidente_c4 <- factor(categorias_anio$categoria_incidente_c4, levels = orden_categorias)
# Crear gráfico barras agrupadas por año
ggplot(categorias_anio, aes(x = categoria_incidente_c4, y = n, fill = factor(anio))) +
  geom_col(position = "dodge") +
  geom_text(aes(label = n), position = position_dodge(width = 0.9), vjust = -0.5, size = 3) +
  scale_fill_manual(values = c("2020" = "green", "2021" = "blue", "2022" = "red")) +
  coord_flip() +
  labs(
    title = "Frecuencia de incidentes por categoria y año",
    x = "Categoría",
    y = "Número de incidentes",
    fill = "Año"
  ) +
  theme_minimal(base_size = 14) +
  theme(plot.title = element_text(hjust = 0.5))

HeatMap por año.

Procederemos a utilizar un mapa de calor, en el cual agruparemos el total deincidentes en las categorías y separaremos por año, para identificar en el total de datos ,si existe algún dato mas relevante, con lo cual descubrimos que hay algunos datos del 2020 los cuales, aunque no afectan al conjunto total de datos si alcanzan a visualizarse.

# Agrupar por año y categoría, excluir NA
df_heatmap <- df %>%
  filter(!is.na(categoria_incidente_c4), !is.na(anio_creacion)) %>%
  count(anio_creacion, categoria_incidente_c4)
# Gráfico heatmap con números y escala roja
ggplot(df_heatmap, aes(x = factor(anio_creacion), y = reorder(categoria_incidente_c4, -n), fill = n)) +
  geom_tile(color = "white") +
  geom_text(aes(label = n), color = "black", size = 3) +  # Añadir números
  scale_fill_gradient(low = "#fee0d2", high = "#de2d26", name = "Incidentes") +
  labs(
    title = "Incidentes - categorías por año",
    x = "Año",
    y = "Categoría"
  ) +
  theme_minimal(base_size = 14) +
  theme(axis.text.x = element_text(size = 12),
        axis.text.y = element_text(size = 10))

2. Día de la semana con más incidentes

Identifica el día de la semana con más incidentes y determinar el total de llamadas para ese día.

Para lograr identificar el día con más incidentes, utilizaremos un gráfico de línea, en el cual agruparemos por día de la semana y numero de accidentes cada conjunto, tomado los diferentes años para diferentes líneas, todo esto con el siguiente código.

Una vez generado, el nuevo grafico de líneas confirma un poco el comportamiento que se menciono en el ejercicio anterior, la información parece tener una disminución en los incidentes reportados, así como, darnos un panorama respecto a los días, con mayores índices de reporte los cuales son viernes sábado y domingo, reduciéndose los lunes nuevamente.

# Procesar: obtener día de la semana
df_dias <- df %>%
  filter(!is.na(fecha_creacion), !is.na(anio_creacion)) %>%
  mutate(dia_semana = wday(fecha_creacion, label = TRUE, abbr = FALSE, week_start = 1)) %>%  # lunes a domingo
  count(anio_creacion, dia_semana)
# Gráfico de líneas con números
ggplot(df_dias, aes(x = dia_semana, y = n, color = factor(anio_creacion), group = anio_creacion)) +
  geom_line(size = 1.2) +
  geom_point(size = 3) +
  geom_text(aes(label = n), vjust = -0.5, size = 4) +
  labs(
    title = "Número de incidentes por día de la semana y por año",
    x = "Día de la semana",
    y = "Número de incidentes",
    color = "Año"
  ) +
  theme_minimal(base_size = 14) +
  theme(axis.text.x = element_text(size = 12))
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

3. Análisis temporal

Crea un análisis temporal que muestre la distribución de incidentes por hora del día para las categorías “DELITO”, “EMERGENCIA” y “URGENCIA MÉDICA”. Utiliza gráficos adecuados para el análisis.

Para cubrir este ejercicio se procederá a generar un mapa de calor, con los totales, agrupados por las categorías de tipo y la marca de tiempo de 0 a 24 hrs con el siguiente código.

En el código notaremos como se tuvo que normalizar para poder realizar el grafico debido al manejo de acentos en la columna, al realizar el agrupamiento, observaremos como los delitos de falta cívica y el horario de entre las 10 pm y las 2 am, suelen ser los más problemáticos en cuanto a este tipo de incidente. Y los servicios entre 10 am y 10 pm

Todos los años

# Cargar datasets con codificación UTF-8
df_1_2022 <- read_csv("llamadas_911_2022_s1.csv", locale = locale(encoding = "UTF-8"), show_col_types = FALSE)
df_2_2021 <- read_csv("llamadas_911_2021_s2.csv", locale = locale(encoding = "UTF-8"), show_col_types = FALSE)
df_1_2021 <- read_csv("llamadas_911_2021_s1.csv", locale = locale(encoding = "UTF-8"), show_col_types = FALSE)
# Unir datasets
df <- bind_rows(df_1_2022, df_2_2021, df_1_2021)
# Normalizar caracteres especiales
df <- df %>%
  mutate(clas_con_f_alarma = iconv(clas_con_f_alarma, from = "UTF-8", to = "ASCII//TRANSLIT"))
# Verificar columnas necesarias
if (!all(c("hora_creacion", "clas_con_f_alarma") %in% names(df))) {
  stop("Faltan columnas necesarias: 'hora_creacion' y/o 'clas_con_f_alarma'")
}
# Extraer hora
df <- df %>%
  mutate(hora = hour(as.POSIXct(hora_creacion, format = "%H:%M:%S")))
# Agrupar y contar
conteo <- df %>%
  group_by(hora, clas_con_f_alarma) %>%
  summarise(incidentes = n(), .groups = "drop")
# Gráfico de calor
ggplot(conteo, aes(x = clas_con_f_alarma, y = factor(hora), fill = incidentes)) + geom_tile(color = "white") +
  geom_text(aes(label = incidentes), color = "black", size = 3) + scale_fill_gradient(low = "white", high = "red") +
  labs(title = "Número de incidentes por hora y clasificación con alarma", x = "Clasificación con alarma",
    y = "Hora del día", fill = "Incidentes") + theme_minimal() + theme(plot.title = element_text(hjust = 0.5, size = 14, face = "bold"))

4.Tiempo promedio de Cierre

Calcula el tiempo promedio entre la creación y cierre del incidente (usa fecha_creacion y fecha_cierre). Así mismo, determinar el tiempo mínimo y máximo.

Para este calculo tomaremos en cuenta la codificación y el formato de los diferentes datos, utilizaremos la fecha_creacion, fecha_cierre, hora_creacion, hora_cierre) deberemos quitar espacios y convertir los parámetros de fecha y hora Al termino obtendremos esta tabla, con la cual podremos realizar el promediosolicitado Sin embargo, este promedio es total, nos interesa saber por categoría cual es el promedio por lo cual actualizaremos nuestro código.

# Unir datasets con su año
df_1_2022 <- read_csv("llamadas_911_2022_s1.csv", locale = locale(encoding = "UTF-8"), show_col_types = FALSE)
df_2_2021 <- read_csv("llamadas_911_2021_s2.csv", locale = locale(encoding = "UTF-8"), show_col_types = FALSE)
df_1_2021 <- read_csv("llamadas_911_2021_s1.csv", locale = locale(encoding = "UTF-8"), show_col_types = FALSE)
df <- bind_rows(df_1_2022, df_2_2021, df_1_2021)
# Limpiar strings y convertir fecha y hora
df <- df %>%
  mutate(
    fecha_creacion = trimws(fecha_creacion),
    hora_creacion = trimws(hora_creacion),
    fecha_cierre = trimws(fecha_cierre),
    hora_cierre = trimws(hora_cierre),
    fecha_hora_creacion = ymd(fecha_creacion) + hms(hora_creacion),
    fecha_hora_cierre = ymd(fecha_cierre) + hms(hora_cierre),
    duracion_minutos = as.numeric(difftime(fecha_hora_cierre, fecha_hora_creacion, units = "mins"))
  )
# Verificar que las fechas están bien convertidas
df %>% select(fecha_creacion, hora_creacion, fecha_hora_creacion) %>% head()
# Calcular estadísticas
resumen <- df %>%
  summarise(
    promedio_min = mean(duracion_minutos, na.rm = TRUE),
    minimo_min = min(duracion_minutos, na.rm = TRUE),
    maximo_min = max(duracion_minutos, na.rm = TRUE)
  )
print(resumen)
## # A tibble: 1 × 3
##   promedio_min minimo_min maximo_min
##          <dbl>      <dbl>      <dbl>
## 1         166.      -55.9   1086041.

Procederemos a realizar un gráfico, de barras para ilustrar mejor el comportamiento, y observar más fácilmente los patrones. Notando que las denuncias, los daños por fenómenos naturales y sismos son los que suelen tardar más en ser atendidos.

# Limpiar strings y convertir fecha y hora
df <- df %>%
  mutate(
    fecha_creacion = trimws(fecha_creacion),
    hora_creacion = trimws(hora_creacion),
    fecha_cierre = trimws(fecha_cierre),
    hora_cierre = trimws(hora_cierre),
    fecha_hora_creacion = ymd(fecha_creacion) + hms(hora_creacion),
    fecha_hora_cierre = ymd(fecha_cierre) + hms(hora_cierre),
    duracion_minutos = as.numeric(difftime(fecha_hora_cierre, fecha_hora_creacion, units = "mins"))
  )
# Verificar que las fechas están bien convertidas
df %>%
  select(fecha_creacion, hora_creacion, fecha_hora_creacion) %>%
  head()
# Calcular promedio de duración por categoría
promedio_por_categoria <- df %>%
  filter(!is.na(duracion_minutos), !is.na(categoria_incidente_c4)) %>%
  group_by(categoria_incidente_c4) %>%
  summarise(promedio_duracion_min = mean(duracion_minutos, na.rm = TRUE),cantidad_incidentes = n()) %>%
  arrange(desc(promedio_duracion_min))  # Opcional: ordenar por duración
# Mostrar resultado
print(promedio_por_categoria)
## # A tibble: 23 × 3
##    categoria_incidente_c4              promedio_duracion_min cantidad_incidentes
##    <chr>                                               <dbl>               <int>
##  1 Sismo                                               2651.                2384
##  2 Denuncia                                             519.              133330
##  3 Cadaver                                              335.                9439
##  4 Accidente                                            240.                  81
##  5 Intento de suicidio                                  224.                2658
##  6 Persona extraviada en zona boscosa                   220.                  80
##  7 Servicios                                            216.              271150
##  8 Derrame o fuga                                       200.               59808
##  9 Danos por fenomeno natural o terce…                  194.               23092
## 10 Abandono                                             175.               14983
## # ℹ 13 more rows
df_por_anio <- df %>%
  filter(!is.na(duracion_minutos), !is.na(categoria_incidente_c4)) %>%
  mutate(anio = year(fecha_hora_creacion)) %>% group_by(anio, categoria_incidente_c4) %>% summarise(promedio_duracion_min = mean(duracion_minutos, na.rm = TRUE),.groups = "drop")
# Gráfico
ggplot(df_por_anio, aes(x = reorder(categoria_incidente_c4, promedio_duracion_min), y = promedio_duracion_min, fill = factor(anio))) +
  geom_col(position = "dodge") +
  coord_flip() +
  labs(
    title = "Duración promedio por categoría y año",
    x = "Categoría",
    y = "Duración promedio (minutos)",
    fill = "Año"
  ) +
  theme_minimal(base_size = 14)

5. Llamadas de Falsa alarma

Para este ejercicio basta con realizar el conteo de los registros con la columna clas_con_f_alarma en FALSA ALARMA y realizar la sumatoria Notaremos que el porcentaje es muy bajo así que procederemos a graficar

# Calcular porcentaje de "Falsa Alarma"
porcentaje_falsa_alarma <- df %>%
  filter(!is.na(clas_con_f_alarma)) %>%
  mutate(falsa_alarma = clas_con_f_alarma == "FALSA ALARMA") %>%
  summarise(
    total_llamadas = n(),
    total_falsas = sum(falsa_alarma),
    porcentaje = total_falsas / total_llamadas * 100
  )

print(porcentaje_falsa_alarma)
## # A tibble: 1 × 3
##   total_llamadas total_falsas porcentaje
##            <int>        <int>      <dbl>
## 1        1671036          504     0.0302

El mejor grafico para ilustrar porcentajes siempre será el grafico pastel por ello será el que realizamos Aunque el porcentaje al ser muy bajo apenas es notorio

df_falsa <- df %>%
  filter(!is.na(clas_con_f_alarma)) %>%
  mutate(clasificacion = ifelse(clas_con_f_alarma == "FALSA ALARMA", "FALSA ALARMA", "Otra")) %>%
  count(clasificacion) %>%
  mutate(porcentaje = n / sum(n) * 100)

ggplot(df_falsa, aes(x = "", y = porcentaje, fill = clasificacion)) +
  geom_col(width = 1) +
  coord_polar(theta = "y") +
  geom_text(
    aes(label = paste0(sprintf("%.3f", porcentaje), "%")),
    position = position_stack(vjust = 0.5),
    color = "white", size = 4
  ) +
  labs(
    title = "Porcentaje de llamadas clasificadas como Falsa Alarma",
    fill = "Clasificación"
  ) +
  theme_void()

Procederemos a realizar un grafico por cada año, para corroborar el comportamiento.

# Asegurarse de que `fecha_hora_creacion` esté correctamente generado
df <- df %>%
  mutate(
    fecha_hora_creacion = ymd(fecha_creacion) + hms(hora_creacion),
    anio = year(fecha_hora_creacion)
  )

# Función para graficar por año
graficar_falsa_alarma <- function(df, anio_target) {
  df_falsa <- df %>%
    filter(!is.na(clas_con_f_alarma), anio == anio_target) %>%
    mutate(clasificacion = ifelse(clas_con_f_alarma == "FALSA ALARMA", "FALSA ALARMA", "Otra")) %>%
    count(clasificacion) %>%
    mutate(porcentaje = n / sum(n) * 100)

  ggplot(df_falsa, aes(x = "", y = porcentaje, fill = clasificacion)) +
    geom_col(width = 1) +
    coord_polar(theta = "y") +
    geom_text(
      aes(label = paste0(sprintf("%.3f", porcentaje), "%")),
      position = position_stack(vjust = 0.5),
      color = "white", size = 4
    ) +
    labs(
      title = paste("Porcentaje Falsa Alarma -", anio_target),
      fill = "Clasificación"
    ) +
    theme_void()
}

# Graficar por año
grafico_2020 <- graficar_falsa_alarma(df, 2020)
grafico_2021 <- graficar_falsa_alarma(df, 2021)
grafico_2022 <- graficar_falsa_alarma(df, 2022)

# Mostrar gráficos (en RStudio, uno por uno o con patchwork/cowplot/gridExtra)
print(grafico_2020)

print(grafico_2021)

print(grafico_2022)

Conclusión

El análisis exploratorio realizado con las librerías dplyr, lubridate y tidyr permitió identificar patrones relevantes en las llamadas al número de emergencias 911 de la CDMX. A través del procesamiento y la transformación de datos, se observaron tendencias temporales clave, como los días y horas con mayor número de incidentes, así como diferencias entre las categorías de atención, destacando la prevalencia de ciertos tipos de emergencias en momentos específicos del día.

Además, se cuantificó el tiempo de atención y la proporción de llamadas clasificadas como falsas alarmas, lo cual proporciona información útil para mejorar la eficiencia del sistema de respuesta.

Este ejercicio evidencia la utilidad de las herramientas de R para el análisis de datos públicos y su potencial para generar conocimiento que contribuya a la toma de decisiones informadas en temas de seguridad y servicios de emergencia.